/**
 * 
 */
package com.google.code.jkippt.http;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.List;

import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicHeader;

import com.google.code.jkippt.auth.Authentication;
import com.google.code.jkippt.auth.HTTPBasicAuth;
import com.google.code.jkippt.auth.TokenAuth;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;

/**
 * @author Federico De Faveri defaveri@gmail.com
 *
 */
public class HttpAPIClient implements APIClient {

	protected static final String API_VERSION = "application/vnd.kippt.20130630+json";
	protected static final String CLIENT_HEADER_VALUE = "JKippt,defaveri@gmail.com,http://federico.defaveri.org";

	protected static final String API_TOKEN_HEADER_NAME = "X-Kippt-API-Token";
	protected static final String USERNAME_HEADER_NAME = "X-Kippt-Username";
	protected static final String CLIENT_HEADER_NAME = "X-Kippt-Client";
	protected static final int DEFAULT_BUFFER_SIZE = 1024 * 4;
	protected static final int EOF = -1;

	protected HttpClient httpClient;
	protected List<Header> headers;
	protected Gson gson;

	public HttpAPIClient(Authentication authentication)
	{
		DefaultHttpClient client = new DefaultHttpClient();
		headers = new ArrayList<Header>();

		//API Client header
		headers.add(new BasicHeader(CLIENT_HEADER_NAME, CLIENT_HEADER_VALUE));
		//API version
		headers.add(new BasicHeader("accept", API_VERSION));

		setAuthentication(client, authentication);

		this.httpClient = client;
		this.gson = new Gson();
	}

	/**
	 * Sets the authentication informations.
	 * @param client
	 * @param authentication
	 */
	protected void setAuthentication(DefaultHttpClient client, Authentication authentication)
	{
		if (authentication instanceof HTTPBasicAuth) {
			HTTPBasicAuth basicAuth = (HTTPBasicAuth) authentication;
			Credentials credentials = new UsernamePasswordCredentials(basicAuth.getUsername(), basicAuth.getPassword());
			client.getCredentialsProvider().setCredentials(AuthScope.ANY, credentials);

		} else if (authentication instanceof TokenAuth) {
			TokenAuth tokenAuth = (TokenAuth) authentication;
			Header usernameHeader = new BasicHeader(USERNAME_HEADER_NAME, tokenAuth.getUsername());
			headers.add(usernameHeader);
			Header tokenHeader = new BasicHeader(API_TOKEN_HEADER_NAME, tokenAuth.getApiToken());
			headers.add(tokenHeader);
		}
	}

	/** 
	 * {@inheritDoc} 
	 */
	@Override
	public String get(String endpoint) throws ServerErrorException, CommunicationException
	{
		HttpResponse httpResponse = doGet(endpoint);
		return processResponse(httpResponse, 200, endpoint);
	}

	protected HttpResponse doGet(String uri) throws CommunicationException
	{
		try {
			HttpGet httpGet = new HttpGet(uri);
			for (Header header:headers) httpGet.addHeader(header);
			HttpResponse httpResponse = httpClient.execute(httpGet);
			return httpResponse;
		} catch(Exception e)
		{
			throw new CommunicationException("An error occurred during GET request to the uri: "+uri, e);
		}
	}	

	/** 
	 * {@inheritDoc} 
	 */
	@Override
	public String post(String endpoint, String content) throws ServerErrorException, CommunicationException
	{
		HttpResponse httpResponse = doPost(endpoint, content);
		return processResponse(httpResponse, 201, endpoint);
	}

	protected HttpResponse doPost(String uri, String content) throws CommunicationException
	{
		try {
			HttpPost httpPost = new HttpPost(uri);
			for (Header header:headers) httpPost.addHeader(header);
			httpPost.setEntity(new StringEntity(content));
			HttpResponse httpResponse = httpClient.execute(httpPost);
			return httpResponse;
		} catch(Exception e)
		{
			throw new CommunicationException("An error occurred during POST request to the uri: "+uri, e);
		}
	}

	/** 
	 * {@inheritDoc} 
	 */
	@Override
	public String put(String endpoint, String content) throws ServerErrorException, CommunicationException
	{
		HttpResponse httpResponse = doPut(endpoint, content);
		return processResponse(httpResponse, 200, endpoint);
	}

	protected HttpResponse doPut(String uri, String content) throws CommunicationException
	{
		try {
			HttpPut httpPut = new HttpPut(uri);
			for (Header header:headers) httpPut.addHeader(header);
			httpPut.setEntity(new StringEntity(content));
			HttpResponse httpResponse = httpClient.execute(httpPut);
			return httpResponse;
		} catch(Exception e)
		{
			throw new CommunicationException("An error occurred during POST request to the uri: "+uri, e);
		}
	}


	/** 
	 * {@inheritDoc} 
	 */
	@Override
	public void delete(String endpoint) throws ServerErrorException, CommunicationException
	{

		HttpResponse httpResponse = doDelete(endpoint);

		StatusLine statusLine = httpResponse.getStatusLine();
		if (statusLine.getStatusCode()!=204) {
			String exceptionMessage = buildExceptionMessage(httpResponse);
			throw new ServerErrorException(exceptionMessage);
		}
	}

	protected HttpResponse doDelete(String uri) throws CommunicationException
	{
		try {
			HttpDelete httpDelete = new HttpDelete(uri);
			for (Header header:headers) httpDelete.addHeader(header);
			HttpResponse httpResponse = httpClient.execute(httpDelete);
			return httpResponse;
		} catch(Exception e)
		{
			throw new CommunicationException("An error occurred during GET request to the uri: "+uri, e);
		}
	}

	protected String processResponse(HttpResponse httpResponse, int expectedStatus, String uri) throws ServerErrorException, CommunicationException
	{
		StatusLine statusLine = httpResponse.getStatusLine();

		if (statusLine.getStatusCode()!=expectedStatus) {
			String exceptionMessage = buildExceptionMessage(httpResponse);
			throw new ServerErrorException(exceptionMessage);
		}

		try {
			String response = getContent(httpResponse);
			return response;
		} catch (Exception e) {
			throw new CommunicationException("An error occurred elaborating the response to the uri: "+uri, e);
		}
	}

	/**
	 * Build the exception message using the information available in the {@link HttpResponse}.
	 * @param response
	 * @return
	 */
	protected String buildExceptionMessage(HttpResponse response)
	{
		StringBuilder exceptionMessage = new StringBuilder();

		exceptionMessage.append("Server error response [status code: ");
		StatusLine statusLine = response.getStatusLine();

		int statusCode = statusLine.getStatusCode();

		exceptionMessage.append(statusCode);
		exceptionMessage.append(", reason: ");
		exceptionMessage.append(statusLine.getReasonPhrase());
		if (statusCode>=400 && statusCode != 500) {
			String errorMessage = getErrorMessage(response);
			exceptionMessage.append(", message: ");
			exceptionMessage.append(errorMessage);
		}
		exceptionMessage.append("]");
		return exceptionMessage.toString();
	}

	/**
	 * Retrieves the error message from the response content.
	 * @param response the request response.
	 * @return the error message or <code>null</code> if there is no parsable error.
	 */
	protected String getErrorMessage(HttpResponse response)
	{
		String jsonMessage;
		try {
			jsonMessage = getContent(response);
		} catch (Exception e) {
			return null;
		}

		if (jsonMessage!=null) {
			JsonElement element = gson.fromJson(jsonMessage, JsonElement.class);
			if (element.isJsonObject()) {
				JsonObject object = element.getAsJsonObject();
				JsonElement messageElement = object.get("message");
				if (messageElement!=null) {
					String message = messageElement.getAsString();
					return message;
				}
			}
		}
		return null;
	}

	/**
	 * Retrieves content from the {@link HttpResponse}
	 * @param httpResponse
	 * @return the content as string, <code>null</code> otherwise.
	 * @throws Exception
	 */
	protected String getContent(HttpResponse httpResponse) throws Exception
	{
		HttpEntity httpEntity = httpResponse.getEntity();

		if (httpEntity!=null) {

			InputStream contentStream = httpEntity.getContent();
			StringWriter output = new StringWriter();
			copy(contentStream, output);
			return output.toString();
		}

		return null;
	}

	protected void copy(InputStream input, Writer output) throws IOException
	{
		InputStreamReader in = new InputStreamReader(input);
		char[] buffer = new char[DEFAULT_BUFFER_SIZE];
		int n = 0;
		while (EOF != (n = in.read(buffer))) {
			output.write(buffer, 0, n);
		}
	}

}
